﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace InnovTech.Util.Crypto
{
    /// <summary>
    /// The utility class to create CryptoStream to encrypt and decrypt the content of a Stream.
    /// </summary>
    /// <remarks>
    /// The pattern:
    /// <code>
    ///     SymmetricAlgorithm provider = CreateSimpleCryptoProvider();
    ///     using (MemoryStream output = new MemoryStream())
    ///     using (CryptoStream cs = Crypt(input, provider.CreateEncrypto(), output))
    ///     {
    ///         byte[] bytes = output.ToArray();
    ///         // Do something with the encrypted array.
    ///     }
    /// </code>
    /// </remarks>
    public static class CryptoUtil
    {
        /// <summary>
        /// Encrypts a string and convert the encrypted content to a byte[].
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public static byte[] Encrypt(String input, SymmetricAlgorithm provider = null)
        {
            if(provider == null)
                provider = CreateDefaultCryptoProvider();

            byte[] inputBytes = Encoding.UTF8.GetBytes(input);
            using (MemoryStream inputStream = new MemoryStream(inputBytes))
            using (MemoryStream outputStream = new MemoryStream())
            using (Crypt(inputStream, provider.CreateEncryptor(), outputStream))
            {
                //Crypt(inputStream, provider.CreateEncryptor(), outputStream);
                return outputStream.ToArray();
            }
        }


        /// <summary>
        /// Decrypts a encryted byte[] to the original string.
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public static String Decrypt(byte[] input, SymmetricAlgorithm provider = null)
        {
            if(provider == null)
                provider = CreateDefaultCryptoProvider();

            using (MemoryStream inputStream = new MemoryStream(input))
            using (MemoryStream outputStream = new MemoryStream())
            using (Crypt(inputStream, provider.CreateDecryptor(), outputStream))
            {
                //Crypt(inputStream, provider.CreateDecryptor(), outputStream);
                return Encoding.UTF8.GetString(outputStream.ToArray());
            }
        }

        /// <summary>
        /// Encrypts a string and convert the encrypted content to a Base64 string.
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public static string EncryptToBase64String(String input, SymmetricAlgorithm provider = null)
        {
            byte[] encrytped = Encrypt(input, provider);
            return System.Convert.ToBase64String(encrytped);
        }

        /// <summary>
        /// Decrypts a string from a encrypted Base64 string.
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public static string DecryptFromBase64String(String input, SymmetricAlgorithm provider = null)
        {
            byte[] toDecrypt = System.Convert.FromBase64String(input);
            return Decrypt(toDecrypt, provider);
        }

        /// <summary>
        /// Creates a CryptoStream to encryt or decrypt the content of the given input Stream to the output Stream 
        /// </summary>
        /// <param name="input"></param>
        /// <param name="transform"></param>
        /// <param name="output"></param>
        /// <returns></returns>
        public static CryptoStream Crypt(Stream input, ICryptoTransform transform, Stream output)
        {
            CryptoStream cryptoStream = new CryptoStream(output, transform, CryptoStreamMode.Write);
            if(input.CanSeek)
                input.Seek(0, SeekOrigin.Begin);

            byte[] bytes = new byte[input.Length];
            int realLength = input.Read(bytes, 0, bytes.Length);

            cryptoStream.Write(bytes, 0, realLength);
            cryptoStream.FlushFinalBlock();
            return cryptoStream;
        }

        public static SymmetricAlgorithm CreateCryptoProvider<T>(byte[] key, byte[] iv) where T : SymmetricAlgorithm
        {
            T provider = Activator.CreateInstance<T>();
            provider.Key = key;
            provider.IV = iv;
            return provider;
        }

        public static SymmetricAlgorithm CreateCryptoProvider<T>() where T : SymmetricAlgorithm
        {
            return CreateCryptoProvider<TripleDESCryptoServiceProvider>(BytesGenerator.Default.GenerateKey(24),
                        BytesGenerator.Default.GenerateKey(8));
        }

        /// <summary>
        /// Creates a simple SymmetricAlgorithm to encrypt or decrypt.
        /// </summary>
        /// <returns></returns>
        private static SymmetricAlgorithm CreateDefaultCryptoProvider()
        {
            return CreateCryptoProvider<TripleDESCryptoServiceProvider>(GenerateAnsiBytes(133, 31, 24),
                GenerateAnsiBytes(19, 137, 8));
        }

        
        /// <summary>
        /// Generates a byte[] for a SymmetricAlgorithm, key or iv.
        /// </summary>
        /// <param name="pswd"></param>
        /// <param name="key"></param>
        /// <param name="size"></param>
        /// <returns></returns>
        public static byte[] GenerateBytes(Guid pswd, byte[] key, int size)
        {
            BytesGenerator bg = new BytesGenerator(pswd, key);
            return bg.GenerateKey(size);
        }

        /// <summary>
        /// Generates a byte[] for a SymmetricAlgorithm, key or iv.
        /// </summary>
        /// <param name="seed"></param>
        /// <param name="additive"></param>
        /// <param name="length"></param>
        /// <returns></returns>
        public static byte[] GenerateAnsiBytes(byte seed, int additive, int length)
        {
            byte[] key = new byte[length];

            for (byte c = seed, i = 0; i < length; i++)
            {
                c = (byte)((c * (i + c) + additive) % 255);
                key[i] = c;
            }
            return key;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        public static byte[] ConvertToPrintable(byte[] source)
        {
            byte[] target = new byte[source.Length];

            for (int i = 0; i < source.Length; i++)
            {
                // only the bytes in (32, 127) are printable.
                target[i] = (byte)(33 + (((float)source[i]) / 255) * 93);
            }
            return target;
        }
    }
}
